/*
 * $Id: MPGraphicContext.java,v 1.3 2003/05/31 07:48:39 milca Exp $
 *
 * Copyright (c) Milan Jedlicka
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

package net.sf.vofce.metapost;

import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import java.awt.font.*;

import java.io.*;

import org.apache.batik.ext.awt.g2d.GraphicContext;
import org.apache.batik.ext.awt.g2d.TransformStackElement;
import org.apache.batik.ext.awt.g2d.TransformType;

/** 
 * Handles the attributes in graphic context and converts it to METAPOST code.<br>
 * Attributes:<br>
 * + Paint
 * + Stroke
 * + Clip
 * + RenderingHints
 * + AffineTransform
 *
 * @author Milan Jedlicka
 *
 */
public class MPGraphicContext extends GraphicContext {

	private static Map colorMap = new HashMap();

	static {
		colorMap.put(Color.black, "black");
		colorMap.put(Color.blue, "blue");
		colorMap.put(Color.green, "green");
		colorMap.put(Color.red, "red");
		colorMap.put(Color.white, "white");
	}

//	DrawOptions
	public static final int DRAWOPTIONS_COLOR = 0;
	
	public static final int DRAWOPTIONS_DASH = 1;
	
	private String[] drawOptions = new String[] { null, null };

//	Transformations
	public static final int TRANSFORM_SCALE = 0;
	public static final int TRANSFORM_TRANSLATE = 1;
	public static final int TRANSFORM_ROTATE = 2;
	public static final int TRANSFORM_SHEAR = 3;

	public static final int OBJECT_TRANSFORM = 0;
	public static final int PEN_TRANSFORM = 1;
	
	private String[] transforms = new String[] { null, null };

// 	Clip
	private int clipCount = 0;
	
	/**
	 * METAPOST syntax generator
	 */
	private MPSyntaxGenerator synGen;

	/**
	 * Writer where to generate METAPOST code.
	 */
	private PrintWriter stream;
	
	/** 
	 * Default constructor
	 * @param 	stream the stream where to generate
	 * @param	synGen the METAPOST syntax generator
	 */
	public MPGraphicContext(MPSyntaxGenerator synGen) {
		super();
		this.synGen = synGen;
	}

	/**
	 * Set stream where to generate code.
	 */
	public void setStream(PrintWriter stream) {
		this.stream = stream;
	}

//-----------------------------------------------------------
//		 Setting graphic context
//-----------------------------------------------------------
	
	/** 
	 * Sets the <code>Paint</code> attribute for the <code>MPGraphics2D</code> context. 
	 * When attribute is set it
	 * generates color descriptor and put it to the <code>stream</code>
	 * @param paint the <code>Paint</code> object to be used to generate
	 * color during the rendering process, or <code>null</code>	
	 */
	public void setPaint(Paint paint) { if (!(getColor().equals((Color)
					paint))) convertColor((Color) paint);
		
		super.setPaint(paint);
	}
	
	/** Sets the <code>Stroke</code> for the <code>MPGraphics2D<code> context. 
	 * When <code>Stroke</code> is set it generates stroke
	 * descriptor and put it to the <code>stream</code> @param s the
	 * <code>Stroke</code> object to be used to stroka a <code>Shape</code>
	 * during rendering process
	 */
	public void setStroke(Stroke s) { if (!(getStroke().equals(s)))
		stream.println(convertStroke(s));
		
		super.setStroke(s);
	}

	/**
	 * Intersects the current clip with the specified rectangle.
	 * @param x the x coordinate of the rectangle to intersect the clip with
	 * @param y the y coordinate of the rectangle to intersect the clip with
	 * @param width the width of the rectangle to intersect the clip with
	 * @param height the height of the rectangle to intersect the clip with
	 */
	public void clipRect(int x, int y, int width, int height) {
		setClip(x, y, width, height);
	}
	
	/**
	 * Sets the current clip to the rectangle specified by the given
	 * coordinates.
	 * @param       x the <i>x</i> coordinate of the new clip rectangle.
	 * @param       y the <i>y</i> coordinate of the new clip rectangle.
	 * @param       width the width of the new clip rectangle.
	 * @param       height the height of the new clip rectangle.
	 */
	public void setClip(int x, int y, int width, int height) {
		super.clip(new Rectangle(x, y, width, height));

		clipCount++;

		int pictCount = this.clipCount - 1;
			
		stream.println( synGen.assignment("picts" + "[" + pictCount + "]", "currentpicture") ); 
		if (pictCount > 0) {
			stream.println( synGen.commandLine("clip", "picts" + "[" + pictCount + "]" + " " 
						+ "to" + " " + "clips" + "[" + pictCount + "]") );
		}
		stream.println( synGen.assignment("currentpicture", "nullpicture") );
	
		stream.println( synGen.assignment("clips" + "[" + clipCount + "]", "unitsquare" + " " 
					+ synGen.scale(width, height) + " "
					+ synGen.shift(synGen.pair(x, y))  + " "
					+ "transformed t" + " " + "transformed default")
				);
	}

	/**
	 * @return	a <code>String</code> representing ending instructions of last clip and all drawing commands for cliped images
	 */
	public String getClipedImages() {

		// End of last clip
		clipCount++;

		int pictCount = this.clipCount - 1;
			
		stream.println( synGen.assignment("picts" + "[" + pictCount + "]", "currentpicture") ); 
		if (pictCount > 0) {
			stream.println( synGen.commandLine("clip", "picts" + "[" + pictCount + "]" + " " + "to" + " " + "clips" + "[" + pictCount + "]") );
		}
		stream.println( synGen.assignment("currentpicture", "nullpicture") );
	
		// Get cliped Images
		String clipedImages = "";
		for (int i = 0; i < clipCount; i++) {
			clipedImages += synGen.commandLine("draw", "picts" + "[" + i + "]"); 
			if (i != clipCount) { clipedImages += "\n"; }
		}
		
		return clipedImages;
	};
	
//-----------------------------------------------------------
//		      Transformations
//-----------------------------------------------------------
	
	/**
	 * Concatenates the current <code>MPGraphics2D</code>
	 * <code>Transform</code> with a rotation transform.
	 * Final transform is converted to METAPOST and put to the <code>stream</code>
	 * @param theta the angle of rotation in radians
	 */
	public void rotate(double theta) {
		super.rotate(theta);
		convertTransform();
		stream.println( synGen.assignment("t", getTransformDescriptor(OBJECT_TRANSFORM)) );
		stream.println( synGen.assignment("pent", getTransformDescriptor(PEN_TRANSFORM)) );
		stream.println( convertStroke(getStroke()) );
	}

	/**
	 * Concatenates the current <code>MPGraphics2D</code>
	 * <code>Transform</code> with a rotation transform around point
	 * Final transform is converted to METAPOST and put to the <code>stream</code>
	 * @param theta the angle of rotation in radians
	 * @param x the <i>x</i> coordinate of point around which rotate
	 * @param y the <i>y</i> coordinate of point around which rotate
	 */
	public void rotate(double theta, double x, double y) {
		super.rotate(theta, x, y);
		convertTransform();
		stream.println( synGen.assignment("t", getTransformDescriptor(OBJECT_TRANSFORM)) );
		stream.println( synGen.assignment("pent", getTransformDescriptor(PEN_TRANSFORM)) );
		stream.println( convertStroke(getStroke()) );
	}
	
	/**
	 * Concatenates the current <code>MPGraphics2D</code>
	 * <code>Transform</code> with a scaling transformation.
	 * Final transform is converted to METAPOST and put to the <code>stream</code>
	 * @param sx the amount by which X coordinates in subsequent rendering operations are
	 * multiplied relative to previous rendering operations
	 * @param sy the amount by which Y coordinates in subsequent rendering operations are
	 * multiplied relative to previous rendering operations.
	 */
	public void scale(double sx, double sy) {
		super.scale(sx, sy);
		convertTransform();
		stream.println( synGen.assignment("t", getTransformDescriptor(OBJECT_TRANSFORM)) );
		stream.println( synGen.assignment("pent", getTransformDescriptor(PEN_TRANSFORM)) );
		stream.println( convertStroke(getStroke()) );
	}

	/**
 	 * Translates the origin of the graphics context to the point
	 * (<i>x</i>,&nbsp;<i>y</i>) in the current coordinate system.
	 * Final transform is converted to METAPOST and put to the <code>stream</code>
	 * @param  x	the <i>x</i> coordinate
	 * @param  y 	the <i>y</i> coordinate
	 */
	public void translate(int x, int y) {
		super.translate(x, y);
		if (x!=0 || y!=0) {
			convertTransform();
			stream.println( synGen.assignment("t", getTransformDescriptor(OBJECT_TRANSFORM)) );
		}
	}

	/**
	 * Concatenates the current <code>MPGraphics2D</code>
	 * <code>Transform</code> with a translating transformation.
	 * Final transform is converted to METAPOST and put to the <code>stream</code>
	 * @param  tx	translate in the <i>x</i> coordinate
	 * @param  ty 	translate in the <i>y</i> coordinate
	 */
	public void translate(double tx, double ty) {
		super.translate(tx, ty);
		convertTransform();
		stream.println( synGen.assignment("t", getTransformDescriptor(OBJECT_TRANSFORM)) );
	}

	/**
	 * Concatenates the current <code>MPGraphics2D</code>
	 * <code>Transform</code> with a shearing transformation.
	 * Final transform is converted to METAPOST and put to the <code>stream</code>
	 * @param  shx	shear in the <i>x</i> coordinate
	 * @param  shy 	shear in the <i>y</i> coordinate
	 */
	public void shear(double shx, double shy) {
		super.shear(shx, shy);
			convertTransform();
			stream.println( synGen.assignment("t", getTransformDescriptor(OBJECT_TRANSFORM)) );
	}


//-----------------------------------------------------------
//		    Context conversion
//-----------------------------------------------------------
	
// 	Color convert

	/**
	 * Converts <code>Color</code> from Java 2D API to METAPOST source code.	
	 * If <code>Color</code> is one of predefined colors then it is converted special way.
	 * Other way the specific <code>Color</code> is converted to METAPOST as red, green and blue component.
	 */
	public void convertColor(Color c) { 
	
		String predefinedColor = (String) colorMap.get(c);
		
		if (predefinedColor == null) {
			double red = c.getRed() / 255d;
			double green = c.getGreen() / 255d;
			double blue = c.getBlue() / 255d;
		
			this.drawOptions[DRAWOPTIONS_COLOR] = synGen.command("withcolor", synGen.triplet(red, green, blue));
		} else {
			this.drawOptions[DRAWOPTIONS_COLOR] = synGen.command("withcolor",  (String) colorMap.get(c));
		}
	}

// 	Stroke convert
		
	/**
	 * Converts <code>Stroke</code> from Java 2D API to METAPOST source code.	
	 * The only class which implements <code>Stroke</code> is <code>BasicStroke</code> 
	 * so every instance of <code>Stroke</code> is re-typed to <code>BasciStroke</code>.
	 * @param s <code>Stroke</code> which to convert
	 * @return a <code>String</code> descriptor of converted <code>Stroke</code>
	 */	
	public String convertStroke(Stroke s) {
		
		String strokeWidth = Float.toString(((BasicStroke) s).getLineWidth());
		String miterLimit = Float.toString( ((BasicStroke) s).getMiterLimit() );
		String capStyle = convertCup(((BasicStroke) s).getEndCap());
		String joinStyle = convertJoin(((BasicStroke) s).getLineJoin());

		String dashPattern = null;
		float[] dashLength = ((BasicStroke) s).getDashArray();		
		
		if ( (dashLength != null) &&  (((BasicStroke) s).getDashArray().length > 0) ) {
			dashPattern = convertDash(((BasicStroke) s).getDashArray()); 
		} else {
			this.drawOptions[DRAWOPTIONS_DASH] = null;
		}
		
		//---------  JAVA 2D API x METAPOST problem --------------
		//  float dashPhase = ((BasicStroke) s).getDashPhase(); //
		//--------------------------------------------------------

		String descriptor = synGen.commandLine("pickup", "pencircle scaled " + strokeWidth + " " 
					+ " transformed pent") + "\n" 
					+ synGen.assignment("linecap", capStyle) + "\n" 
					+ synGen.assignment("linejoin", joinStyle) + "\n"
					+ synGen.assignment("miterlimit", miterLimit);
		
		if (dashPattern != null) { descriptor += dashPattern + "\n"; }
		
		return descriptor;
	}


// Stroke cup convert

	/**
	 * Converts style of brush cup from Java 2D API to MetaPost source code.	
	 * @param capStyle a style of cup
	 * @return 	   a <code>String</code> descriptor of converted cup style
	 */	
	public String convertCup(int capStyle) {
		switch(capStyle) {
			case BasicStroke.CAP_BUTT:
			return "butt";
			case BasicStroke.CAP_ROUND:
			return "rounded";
			default:
			case BasicStroke.CAP_SQUARE:
			return "squared";
		}
	}


// Line join convert

	/**
	 * Converts line joining style from Java 2D API to MetaPost source code.	
	 * @param joinStyle a style of line joining
	 * @return 	    a <code>String</code> descriptor of line join style
	 */	
	public String convertJoin(int joinStyle) {
		switch(joinStyle){
			case BasicStroke.JOIN_BEVEL:
			return "beveled";
			case BasicStroke.JOIN_ROUND:
			return "rounded";
			case BasicStroke.JOIN_MITER:
			default:
			return "mitered";
		}
	}

// Stroke dash convert

	/**
	 * Converts dash of brush from Java 2D API to MetaPost source code.	
	 * @param dash	field of dash to convert	
	 * @return 	a <code>String</code> descriptor of dash
	 */ 	
	public String convertDash(float[] dash) {

		String dashP = "";
		
		for (int i = 0; i < dash.length; i++) {
			if ((i % 2) == 0) { dashP += "on "; } else { dashP += "off "; }
			dashP += Float.toString(dash[i]) + " ";
		}

		
		if ((dash.length % 2) != 0) {
			for (int i = 0; i < dash.length; i++) {
				if ((i % 2) == 0) { dashP += "off "; } else { dashP += "on "; }
				dashP += Float.toString(dash[i]) + " ";
			}
		}
		
		this.drawOptions[DRAWOPTIONS_DASH] = "dashed dash";
	
		return synGen.declaration("picture", "dash") + "\n"
			+ synGen.assignment("dash", synGen.procedure("dashpattern", dashP));
	}

	/**
	 * @return	a <code>String</code> representing METAPOST drawoptions which contains: <br>
	 *		+ Color to use <br>
	 * 		+ Dash to use
	 */	
	public String drawOptionsDescriptor() {
		
		String descriptor = "";
		
		for (int i = 0; i < this.drawOptions.length; i++) {
			if (drawOptions[i] != null) { descriptor += drawOptions[i] + " "; }
		}
		
		return descriptor;
	}
	
//	Transformations

	/**
	 * Converts all basic transforms to MetaPost source code. 
	 * Concatenates basic transforms of the same type to one transform. <br>
	 * Supported transforms: <br>
	 * + scale <br>
	 * + translate <br>
	 * + rotate <br>
	 */
	public void convertTransform() {	

		TransformStackElement[] stack =  getTransformStack();
		
		Vector optimStack = new Vector();
	
		// Transform Stack Optimalization		
		int i = stack.length;
		int optimStackSize = 0;

		TransformStackElement transStackElement = (TransformStackElement) stack[i-1].clone();
		TransformStackElement optimStackElement;
		
		optimStack.add(transStackElement);		
		
		while (i > 1) {
			i--;
			optimStackSize = optimStack.size();
			transStackElement = (TransformStackElement) stack[i-1].clone();
			optimStackElement = (TransformStackElement) optimStack.elementAt(optimStackSize-1);
		
			if ( (optimStackElement.getType().toInt() == transStackElement.getType().toInt())  
					&& (transStackElement.getType().toInt() != TRANSFORM_SHEAR) ) {
				optimStackElement.concatenate(transStackElement);
			} else {
				optimStack.addElement(transStackElement);
			}
		}

		// Trasform Stack convertion (to MetaPost code)
		String transformDescriptor = "identity ";
		String penTransformDescriptor = "identity ";
		
		TransformStackElement[] optimTransField = new TransformStackElement[optimStack.size()];
		optimStack.copyInto(optimTransField);

		for (int j = 0; j < optimTransField.length; j++) {
			double[] parameters = optimTransField[j].getTransformParameters();
			switch(optimTransField[j].getType().toInt()) {
				case TransformType.TRANSFORM_SCALE:
					transformDescriptor += synGen.scale(parameters[0], parameters[1]);
					penTransformDescriptor += synGen.scale(parameters[0], parameters[1]);
					break;
				case TransformType.TRANSFORM_ROTATE:
					transformDescriptor +=  synGen.rotate(Math.toDegrees(parameters[0]));
					penTransformDescriptor +=  synGen.rotate(-Math.toDegrees(parameters[0]));
					break;
				case TransformType.TRANSFORM_TRANSLATE:
					transformDescriptor += synGen.shift(synGen.pair(parameters[0], parameters[1]));
					break;
				case TransformType.TRANSFORM_SHEAR:
					transformDescriptor += synGen.rotate(-90) + synGen.shear(-parameters[1]) 
							    +  synGen.rotate(90) + synGen.shear(parameters[0]);
					break;
			}
		}
		
		this.transforms[OBJECT_TRANSFORM] = transformDescriptor;
		this.transforms[PEN_TRANSFORM] = penTransformDescriptor;
	}
		
	/**
	 * @return 	a <code>String</code> representing METAPOST transform descriptor. 
	 *		It isn't assigned to specific variable, so it needs to be assigned 
	 *		in upper methods. 
	 */
	public String getTransformDescriptor(int witch) {
		
		if (witch == OBJECT_TRANSFORM) {
			return transforms[OBJECT_TRANSFORM];
		} else if (witch == PEN_TRANSFORM) {
			return transforms[PEN_TRANSFORM];
		}

		return "identity";
	} 	
}
